import { AuditResult, AuditOk, AuditFail } from './common';

/**
 * Renders the provided audit results to well-formatted and valid HTML.
 *
 * Do note that the rendered result is not an HTML document, it's rather
 * just a component with results.
 */
export async function renderAuditResultsToHTML(results: AuditResult[]) {
  const grouped = {
    total: 0,
    ok: [] as AuditOk[],
    notice: [] as AuditFail[],
    warn: [] as AuditFail[],
    error: [] as AuditFail[],
  };
  for (const result of results) {
    grouped.total++;
    if (result.status === 'ok') {
      grouped[result.status].push(result);
    } else {
      grouped[result.status].push(result);
    }
  }

  let report = '<i>* This report was auto-generated by graphql-http</i>\n';
  report += '\n';

  report += '<h1>GraphQL over HTTP audit report</h1>\n';
  report += '\n';

  report += '<ul>\n';
  report += `<li><b>${grouped.total}</b> audits in total</li>\n`;
  // font-family: monospace helps render native emojis in HTML
  if (grouped.ok.length) {
    report += `<li><span style="font-family: monospace">✅</span> <b>${grouped.ok.length}</b> pass</li>\n`;
  }
  if (grouped.notice.length) {
    report += `<li><span style="font-family: monospace">💡</span> <b>${grouped.notice.length}</b> notices (suggestions)</li>\n`;
  }
  if (grouped.warn.length) {
    report += `<li><span style="font-family: monospace">❗️</span> <b>${grouped.warn.length}</b> warnings (optional)</li>\n`;
  }
  if (grouped.error.length) {
    report += `<li><span style="font-family: monospace">❌</span> <b>${grouped.error.length}</b> errors (required)</li>\n`;
  }
  report += '</ul>\n';
  report += '\n';

  if (grouped.ok.length) {
    report += '<h2>Passing</h2>\n';
    report += '<ol>\n';
    for (const [, result] of grouped.ok.entries()) {
      report += `<li><code>${result.id}</code> ${result.name}</li>\n`;
    }
    report += '</ol>\n';
    report += '\n';
  }

  if (grouped.notice.length) {
    report += `<h2>Notices</h2>\n`;
    report +=
      'The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n';
    report += '<ol>\n';
    for (const [, result] of grouped.notice.entries()) {
      report += await printAuditFail(result);
    }
    report += '</ol>\n';
    report += '\n';
  }

  if (grouped.warn.length) {
    report += `<h2>Warnings</h2>\n`;
    report += 'The server <i>SHOULD</i> support these, but is not required.\n';
    report += '<ol>\n';
    for (const [, result] of grouped.warn.entries()) {
      report += await printAuditFail(result);
    }
    report += '</ol>\n';
    report += '\n';
  }

  if (grouped.error.length) {
    report += `<h2>Errors</h2>\n`;
    report += 'The server <b>MUST</b> support these.\n';
    report += '<ol>\n';
    for (const [, result] of grouped.error.entries()) {
      report += await printAuditFail(result);
    }
    report += '</ol>\n';
  }

  return report;
}

async function printAuditFail(result: AuditFail) {
  let report = '';
  report += `<li><code>${result.id}</code> ${result.name}\n`;
  report += '<details>\n';
  report += `<summary>${truncate(result.reason)}</summary>\n`;
  report += '<pre><code class="lang-json">'; // no "\n" because they count in HTML pre tags
  const res = result.response;
  const headers: Record<string, string> = {};
  for (const [key, val] of res.headers.entries()) {
    // some headers change on each run, dont report it
    if (['date', 'expires'].includes(key)) {
      headers[key] = '<timestamp>';
    } else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
      headers[key] = '<omitted>';
    } else {
      headers[key] = val;
    }
  }
  let text = '',
    json;
  try {
    text = await res.text();
    json = JSON.parse(text);
    // is json, there shouldnt be nothing to sanitize (hopefully)
  } catch {
    // is not json, avoid rendering html (rest is allowed)
    if (res.headers.get('content-type')?.includes('text/html')) {
      text = '<html omitted>';
    }
  }
  const stringified = JSON.stringify(
    {
      status: res.status,
      statusText: res.statusText,
      headers,
      body: json || (text?.length > 5120 ? '<body is too long>' : text) || null,
    },
    (_k, v) => {
      if (v != null && typeof v === 'object' && !Array.isArray(v)) {
        // sort object fields for stable stringify
        const acc: Record<string, unknown> = {};
        return Object.keys(v)
          .sort()
          .reverse() // body on bottom
          .reduce((acc, k) => {
            acc[k] = v[k];
            return acc;
          }, acc);
      }
      return v;
    },
    2,
  );
  report += stringified + '\n';
  report += '</code></pre>\n';
  report += '</details>\n';
  report += '</li>\n';

  return report;
}

function truncate(str: string, len = 1024) {
  if (str.length > len) {
    return str.substring(0, len) + '...';
  }
  return str;
}
